By Diego Marinho de Oliveira on 12-08-15
The travelling salesman problem (TSP) is a well know problem in Computer Science and Math field. It consist in determine given a list of cities and distance between these cities what is the shortest path that starting from an arbitrary city we could visit all other cities only once and return to the initial city.
As it is a NP-hard problem in combinatorial optimization realm, plausible solutions make use of approximations. Thus, we could use Genetic Algorithm to search for a feasible solution in a finite amount of time since it is a search heristic that mimics the process of natural selection.
Then, in this short notebook, we will explore the use of GeneticAlgorithms package in Julia to find a fesiable solution to TSP.
Lets supose that we are from the ZMData logistic company that works on California, US and want to distribute our new high-tech devices in five cities: 1. San Francisco, 2. Palo Alto, 3. Los Angeles, 4. San Diego and 5.Las Vegas. The cities, routes and costs is represented in Figure 1. We want to find a good route that save us money and time!
How we can do that using a good approximation? We could use genetic algorithms (GA)! Lets model our problem using one nice library on Julia called GeneticAlgorithms
.
The first step is to install GeneticAlgorithms package. Just simple run these below commands at your Julia REPL (Read, Evaluate, Print and Loop) console:
Pkg.add("GeneticAlgorithms")
Pkg.update();
Then import the package in your notebook as follow:
In [1]:
using GeneticAlgorithms
The package can be used to solve any problem that we could model as an optimization problem For our case, our space problem is the all possible routes for the five cities. To model our specific problem we need to define:
The first step in our modelling is defining a entity of the population. An entity in our problem is a route that has as attribute the edges that connects the cities. Then we can represent our entity named Route
in Julia by
In [ ]:
type Route <: Entity
edges::Array
fitness
Route() = new(get_random_path(N_VERTICES))
Route(edges) = new(edges)
end
Lets understand your model. By default we must inherit the abstract GeneticAlgorithms.Entity from the GA package. Also, we need to set fitness as an internal attribute that can be accessed by the framework in other inherited methods from Entity. We also define 2 Constructors. One receives the edges and the another one generate a random path with a constant N_VERTICES
that we will define later on the notebook.
The fitness function will be used by the framework and expect an entity and returns a score. For your case the fitness function will by
In [ ]:
function fitness(ent)
sum(map(e -> COSTS[first(e), last(e)], ent.edges))
end
Also we need to define a comparison function to indicates what is the best solution from the previous and current one. The best score in our case is the one that has a lower score. Thus, we define as
In [ ]:
function GeneticAlgorithms.isless(lhs::Route, rhs::Route)
abs(lhs.fitness) > abs(rhs.fitness)
end
Before we make the crossover on the entities we need to group them by some criteria. As in our example we don't have any specific restriction we define a naive one as following
In [ ]:
function group_entities(pop)
if pop[1].fitness <= STOP_COST
return
end
# simple naive groupings that pair the best entitiy with every other
for i in 1:length(pop)
produce([1, i])
end
end
Also here we define a criteria to stop making groups and interrupt the process of searching for new solutions. Then, we define a constant named STOP_COST
.
Crossover is a basic operator in GA that combines instances from the population producing a new generation. Then, we propose the use of crossover order to preserve the order from the parents in the new generation. Also we limitated the crossover between the two instances from the group.
In [4]:
# CrossOver OX (Based on source: http://bit.ly/1XU0HWN)
function crossover(group)
child = Route()
num_parents = length(group)
num_parents < 2 && return child
vertices_1 = edges2vertices(group[1].edges)
vertices_2 = edges2vertices(group[2].edges)
n1, n2 = shuffle(collect(1:length(vertices_1)))[1:2]
e1, e2 = min(n1, n2)[1], max(n1, n2)[1]
fixed_vertices = vertices_1[e1:e2]
vertices_1 = filter(v -> !(v in fixed_vertices), vertices_1)
new_vertices = []
count_fixed, count_1, count_2 = 1, 1, 1
for i=1:length(vertices_2)
if e1 <= i <= e2
push!(new_vertices, fixed_vertices[count_fixed])
count_fixed += 1
else
if !(vertices_2[count_2] in fixed_vertices) &&
!(vertices_2[count_2] in new_vertices)
push!(new_vertices, vertices_2[count_2])
vertices_1 = filter(v-> v != vertices_2[count_2], vertices_1)
count_2 += 1
else
push!(new_vertices, vertices_1[count_1])
vertices_2 = filter(v-> v != vertices_1[count_1], vertices_2)
count_1 += 1
end
end
end
child.edges = vertices2edges(new_vertices)
return child
end
Out[4]:
The mutation act directly on a single entity and is responsable to produce some changes by chance over a new generation. We define ours by
In [ ]:
function mutate(ent)
# Mutate only in 1% of the time
rand(Float64) < 0.99 && return
vertices = edges2vertices(ent.edges)
# mutate
n1 = rand(collect(1:length(vertices)), 1)
n2 = rand(collect(1:length(vertices)), 1)
vertices[n1], vertices[n2] = vertices[n2], vertices[n1]
ent.edges = vertices2edges(vertices)
end
Our mutations occurs only 1% of the times that occurs a crossover. We decided to swap random vertices from the initial route generated in crossover.
After defined our six points of modelling over the framework we are ready to package our solution in a module named OurTSPMathModel
. Then, the summary of our modelling above can be shown by
In [18]:
module OurTSPMathModel
using GeneticAlgorithms
# Constants w/ vertices number and cost of our routes
const N_VERTICES = 5
const COSTS = [
0 22 41 44 50;
22 0 40 22 31;
41 40 0 22 42;
44 22 22 0 22;
50 31 42 22 0];
type Route <: Entity
edges::Array
fitness
Route() = new(get_random_path(N_VERTICES))
Route(edges) = new(edges)
end
edges2vertices(edges) = map(e -> first(e), edges)
function vertices2edges(vertices)
edges = map(i -> (vertices[i], vertices[i+1]), vcat(1:length(vertices)-1))
push!(edges, (edges[end][2], edges[1][1]))
end
function get_random_path(n)
vertices = shuffle(collect(1:n))
vertices2edges(vertices)
end
function create_entity(num)
edges = get_random_path(N_VERTICES)
Route(edges)
end
function fitness(ent)
sum(map(e -> COSTS[first(e), last(e)], ent.edges))
end
function GeneticAlgorithms.isless(lhs::Route, rhs::Route)
abs(lhs.fitness) > abs(rhs.fitness)
end
function group_entities(pop)
if pop[1].fitness <= 150
return
end
# simple naive groupings that pair the best entitiy with every other
for i in 1:length(pop)
produce([1, i])
end
end
# CrossOver OX (Based on source: http://bit.ly/1XU0HWN)
function crossover(group)
child = Route()
num_parents = length(group)
num_parents < 2 && return child
vertices_1 = edges2vertices(group[1].edges)
vertices_2 = edges2vertices(group[2].edges)
n1, n2 = shuffle(collect(1:length(vertices_1)))[1:2]
e1, e2 = min(n1, n2)[1], max(n1, n2)[1]
fixed_vertices = vertices_1[e1:e2]
vertices_1 = filter(v -> !(v in fixed_vertices), vertices_1)
new_vertices = []
count_fixed, count_1, count_2 = 1, 1, 1
for i=1:length(vertices_2)
if e1 <= i <= e2
push!(new_vertices, fixed_vertices[count_fixed])
count_fixed += 1
else
if !(vertices_2[count_2] in fixed_vertices) &&
!(vertices_2[count_2] in new_vertices)
push!(new_vertices, vertices_2[count_2])
vertices_1 = filter(v-> v != vertices_2[count_2], vertices_1)
count_2 += 1
else
push!(new_vertices, vertices_1[count_1])
vertices_2 = filter(v-> v != vertices_1[count_1], vertices_2)
count_1 += 1
end
end
end
child.edges = vertices2edges(new_vertices)
return child
end
function mutate(ent)
# Mutate only in 1% of the time
rand(Float64) < 0.99 && return
vertices = edges2vertices(ent.edges)
# mutate
n1 = rand(collect(1:length(vertices)), 1)
n2 = rand(collect(1:length(vertices)), 1)
vertices[n1], vertices[n2] = vertices[n2], vertices[n1]
ent.edges = vertices2edges(vertices)
end
end
Out[18]:
As a matter of simplification we added the costs and vertices number in the beginning of the module. Also we defined some auxiliar functions to better encapsulate responsabilities and facilitate the reuse of code. For example, edges2vertices
, vertices2edges
and get_random_path
are functions that were not commented at section 2 but are of importance to the all GA algorithm works. They represents transformation from edges to vertices, vertices to edges and generate random routes respectively.
Is very easy to run our GA model. First we need to import the GeneticAlgorithms
, create our model and call the population
method.
In [13]:
using GeneticAlgorithms
In [25]:
model = runga(OurTSPMathModel; initial_pop_size = 8)
population(model)
Out[25]:
From the above results we can see that it generates 8 outputs that corresponds each one from the initial instance from the initial population. By observation we see that each run produces differents results and this is due by the crossover and mutation characteristic of the algorithm. Also we can observes that for this time the best route is:
[(3,1),(1,2),(2,4),(4,5),(5,3)] with cost 149 and is shown by Figure 2.
In [ ]:
Supposing that the best solution is 138, we can calculate the gap between then by $\frac{Opt-He}{Opt} \times 100 = -7.98$. That is not bad! If we were more paciente we could run more trials and draw the best solution from the proposed solutions shown.
This notebook was the aim of showing how to use GA algorithms in Julia. Also wanted to introduce new concepts and highlights how simple can be solve problems in Julia.
Oliveira, Diego M. is a Ph.D. candidate in Applyed Mathematics and Modelling by Universidade Aberta de Lisboa and has a Master in Computer Science by Universidade Federal de Minas Gerais. He has passion with Data Science and has broad experience in many fields as Natural Language Processing, Recommendation Systems, Machine Learning, Software Engineering, Statistics and Mathematics. Publish almost daily posts about Machine Learning on Linkedin and PracticalLearning.io. Has ZMData as a consulting company in Machine Learning, Data Science and Big Data problems and works for clients around the globe (US, Europe, among others).